﻿// Copyright (c) 2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

using System;
using System.Collections.Generic;
using System.Text;
using System.ComponentModel;
using System.Windows.Forms;
using System.Drawing;
using System.Drawing.Imaging;

namespace Util.Forms
{
    /// <summary>
    /// Wrapper around a Windows.Forms.ImageList to provide rich 
    /// functionalities.
    /// </summary>
    public class FramedImageList : Component
    {
        /// <summary>
        /// Dummy bitmap to erase the background of an image list slot.
        /// </summary>
        private static Bitmap dummyBitmap = new Bitmap(1, 1, PixelFormat.Format24bppRgb);

        /// <summary>
        /// The underlying native .NET ImageList component; null if disposed.
        /// </summary>
        private ImageList nativeImageList;

        /// <summary>
        /// The pen used to draw an image's frame. If the pen's width is 0,
        /// the frame is not drawn.
        /// </summary>
        private Pen framePen;

        /// <summary>
        /// The space (in pixels) to leave blank outside the image frame.
        /// </summary>
        private int frameMargin;

        /// <summary>
        /// The space (in pixels) to leave blank inside the image frame.
        /// </summary>
        private int framePadding;

        /// <summary>
        /// Creates a SmartImageList.
        /// </summary>
        public FramedImageList()
        {
            nativeImageList = new ImageList();
            nativeImageList.ColorDepth = ColorDepth.Depth32Bit;
            nativeImageList.TransparentColor = Color.Empty;
            framePen = new Pen(Color.Black, 0);
            framePen.Alignment = System.Drawing.Drawing2D.PenAlignment.Inset;
            frameMargin = 0;
            framePadding = 0;
        }

        /// <summary>
        /// Creates a SmartImageList and associates it with a container.
        /// </summary>
        /// <param name="container">The container to add this component to.
        /// </param>
        public FramedImageList(IContainer container)
            : this()
        {
            container.Add(this);
        }

        /// <summary>
        /// Disposes this component.
        /// </summary>
        /// <param name="disposing">true to dispose managed resources.</param>
        protected override void Dispose(bool disposing)
        {
            if (disposing && nativeImageList != null)
            {
                nativeImageList.Dispose();
                framePen.Dispose();
                nativeImageList = null;
                framePen = null;
            }
            base.Dispose(disposing);
        }

        private void CheckDisposed()
        {
            if (nativeImageList == null)
                throw new ObjectDisposedException("FramedImageList");
        }

        private void AdjustImageListSize(int increment)
        {
            if (increment != 0)
            {
                Size sz = nativeImageList.ImageSize;
                sz.Width += increment * 2;
                sz.Height += increment * 2;
                nativeImageList.ImageSize = sz;
            }
        }

        /// <summary>
        /// Gets the underlying native .NET ImageList component. The native
        /// ImageList component can be assigned to controls such as ListView
        /// and TreeView. If this object has been disposed, returns null.
        /// </summary>
        [Browsable(false)]
        public ImageList NativeImageList
        {
            get { return nativeImageList; }
        }

        /// <summary>
        /// Gets the number of images in this image list.
        /// </summary>
        [Browsable(false)]
        public int ImageCount
        {
            get { CheckDisposed(); return nativeImageList.Images.Count; }
        }

        /// <summary>
        /// Gets or sets the size of an image, excluding the frame.
        /// </summary>
        [Description("Size of an image, excluding the frame if any.")]
        public Size ImageSize
        {
            get
            {
                CheckDisposed();
                Size sz = nativeImageList.ImageSize;
                int w = this.FrameWidth + frameMargin + framePadding;
                return new Size(sz.Width - w * 2, sz.Height - w * 2);
            }
            set
            {
                if (value.Width <= 0 || value.Height <= 0)
                    throw new ArgumentException("Size must not be empty.");
                CheckDisposed();

                if (value != this.ImageSize)
                {
                    int w = this.FrameWidth + frameMargin + framePadding;
                    nativeImageList.ImageSize = 
                        new Size(value.Width + w * 2, value.Height + w * 2);
                }
            }
        }

        /// <summary>
        /// Gets or sets the color of an image's frame.
        /// </summary>
        [Description("Width in pixels of an image's frame; 0 = no frame.")]
        [DefaultValue(typeof(Color), "0, 0, 0")]
        public Color FrameColor
        {
            get { CheckDisposed(); return framePen.Color; }
            set { CheckDisposed(); framePen.Color = value; }
        }

        /// <summary>
        /// Gets or sets the width of an image's frame. A value of zero
        /// indicates no frame.
        /// </summary>
        [Description("Width in pixels of an image's frame; 0 = no frame.")]
        [DefaultValue(0)]
        public int FrameWidth
        {
            get { CheckDisposed(); return (int)framePen.Width; }
            set
            {
                CheckDisposed();
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(
                        "value", "FrameWidth must be greater than or equal to zero.");
                }
                AdjustImageListSize(value - (int)framePen.Width);
                framePen.Width = value;
            }
        }

        /// <summary>
        /// Gets or sets the margin outside an image's frame.
        /// </summary>
        [Description("Margin in pixels on the outside of an image's frame.")]
        [DefaultValue(0)]
        public int FrameMargin
        {
            get { CheckDisposed(); return frameMargin; }
            set
            {
                CheckDisposed();
                if (value < 0)
                {
                    throw new ArgumentOutOfRangeException(
                        "value", "FrameMargin must be non-negative.");
                }
                AdjustImageListSize(value - frameMargin);
                frameMargin = value;
            }
        }

        /// <summary>
        /// Gets or sets the padding inside an image's frame.
        /// </summary>
        [Description("Padding in pixels on the inside of an image's frame.")]
        [DefaultValue(0)]
        public int FramePadding
        {
            get { CheckDisposed(); return framePadding; }
            set
            {
                CheckDisposed();
                if (value < 0)
                {
                    throw new ArgumentException("FramePadding must be non-negative.");
                }
                AdjustImageListSize(value - framePadding);
                framePadding = value;
            }
        }

        public void AddImage(Image image)
        {
            CheckDisposed();

            int index = nativeImageList.Images.Count;

            // Add an opaque image to the list. This ensures the ImageList
            // will not treat an image at this slot as transparent, so that
            // future requests to render this image can be handled correctly.
            nativeImageList.Images.Add(dummyBitmap);

            // Draw the frame, magin, and padding of the image.
            if (this.FrameWidth + frameMargin + framePadding > 0)
            {
                using (Bitmap bmpActual = nativeImageList.GetInPlaceBitmap(index))
                using (Graphics g = Graphics.FromImage(bmpActual))
                {
                    g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;

                    // Create a rectangle of the size of the whole image.
                    Rectangle rect = new Rectangle(
                        new Point(0, 0), nativeImageList.ImageSize);

                    // Clear the margin and padding area.
                    if (framePadding + frameMargin > 0)
                    {
                        g.FillRectangle(Brushes.Transparent, rect);
                    }

                    // Draw the frame if any.
                    if (framePen.Width > 0)
                    {
                        rect = new Rectangle(
                            frameMargin, 
                            frameMargin, 
                            rect.Width - 2 * frameMargin, 
                            rect.Height - 2 * frameMargin);
                        g.DrawRectangleAccurately(framePen, rect);
                    }
                }
            }

            // Draw the image within the frame.
            SetImage(index, image);
        }

        /// <summary>
        /// Gets an image at the given location. The returned object is a
        /// bitmap that directs wraps the underlying buffer, therefore
        /// changes to this bitmap will change the image in the list.
        /// </summary>
        /// <param name="index"></param>
        /// <param name="image"></param>
        // TODO: implement Images[..] to return the actual native bitmap.
        public Image GetImage(int index)
        {
            CheckDisposed();
            if (index < 0 || index >= nativeImageList.Images.Count)
                throw new ArgumentOutOfRangeException("index");

            // Get the bounding rectangle of the image excluding the frame.
            int w = this.FrameWidth + frameMargin + framePadding;
            Size sz = this.ImageSize;
            Rectangle rect = new Rectangle(new Point(w, w), sz);

            return nativeImageList.GetInPlaceBitmap(index, rect);
        }

        /// <summary>
        /// Sets an image at the given location.
        /// </summary>
        /// <param name="index"></param>
        /// <param name="image"></param>
        // TODO: implement Images[..] to return the actual native bitmap.
        public void SetImage(int index, Image image)
        {
            CheckDisposed();
            if (index < 0 || index >= nativeImageList.Images.Count)
                throw new ArgumentOutOfRangeException("index");

            // Get the bounding rectangle of the image excluding the frame.
            int w = this.FrameWidth + frameMargin + framePadding;
            Size sz = this.ImageSize;
            Rectangle rect = new Rectangle(new Point(w, w), sz);

            using (Bitmap bmpActual = nativeImageList.GetInPlaceBitmap(index, rect))
            using (Graphics g = Graphics.FromImage(bmpActual))
            {
                g.CompositingMode = System.Drawing.Drawing2D.CompositingMode.SourceCopy;

                // Clear the background, because DrawImageCenteredAndFitted
                // leaves the unpainted area untouched.
                rect = new Rectangle(new Point(0, 0), sz);
                g.FillRectangle(Brushes.Transparent, rect);

                // Draw the image.
                g.DrawImageCenteredAndFitted(image, rect);
            }
        }
    }
}
